<!DOCTYPE html>
<html>

<head>
    <title>hello world</title>
    <style>
        #main-page {
            width: 100%;
            overflow: hidden;
        }

        #main-page::after {
            clear: both;
        }

        #main-left {
            width: 30%;
            float: left;
        }

        #main-right {
            width: 70%;
            float: right;
        }

        #user-list {
            width: 300px;
            padding: 0px;
            margin: 0px;
        }

        #user-list li {
            padding: 2.5px 10px;
            border: 1px #ccc solid;
            list-style: none;
            margin-bottom: 5px;
            background-color: azure;
        }

        #user-list li dl {
            overflow: hidden;
        }

        #user-list li dl dd {
            float: left;
        }
    </style>
    <script src="//cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.js"></script>
    <script src="//cdn.bootcdn.net/ajax/libs/socket.io/3.0.4/socket.io.js"></script>
</head>

<body>

    <div id="main-page">

        <div id="main-left">
            <div id="me">
                hello
            </div>
            <div class='user-list'>
                <ul id="user-list">
                    <!-- <li id="user-test">
                        <dl>
                            <dt>User Test</dt>
                            <dd><button class="call">发起通话</button></dd>
                            <dd>私聊</dd>
                        </dl>
                    </li> -->
                </ul>
            </div>

        </div>
        <div id="main-right">
            <div id="local-video">
                <video autoplay controls id="video-local"></video>
            </div>

            <!-- <video autoplay controls id="video-remote"></video> -->
            <div id="videos"></div>
        </div>


    </div>

    <script>
        //封装一部分函数
        function getUserMedia(constrains, success, error) {
            let promise;
            if (navigator.mediaDevices.getUserMedia) {
                //最新标准API
                promise = navigator.mediaDevices.getUserMedia(constrains).then(success).catch(error);
            } else if (navigator.webkitGetUserMedia) {
                //webkit内核浏览器
                promise = navigator.webkitGetUserMedia(constrains).then(success).catch(error);
            } else if (navigator.mozGetUserMedia) {
                //Firefox浏览器
                promise = navagator.mozGetUserMedia(constrains).then(success).catch(error);
            } else if (navigator.getUserMedia) {
                //旧版API
                promise = navigator.getUserMedia(constrains).then(success).catch(error);
            }
            return promise;
        }

        function canGetUserMediaUse() {
            return !!(navigator.mediaDevices.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia);
        }

        //STUN,TURN服务器配置参数
        const iceServer = {
            iceServers: [{ urls: ["stun:ss-turn1.xirsys.com"] }, { username: "CEqIDkX5f51sbm7-pXxJVXePoMk_WB7w2J5eu0Bd00YpiONHlLHrwSb7hRMDDrqGAAAAAF_OT9V0dWR1d2Vi", credential: "446118be-38a4-11eb-9ece-0242ac140004", urls: ["turn:ss-turn1.xirsys.com:80?transport=udp", "turn:ss-turn1.xirsys.com:3478?transport=udp"] }]
        };

        //本地视频的对象
        const localVideoElm = document.getElementById("video-local");

        //本地流
        var localStream = null;
        //存放RTCPeerConnection的数组
        var pc = [];

        function init(socket_id) {

            //本地流
            if (localStream) {
                //
            } else {
                if (canGetUserMediaUse()) {
                    getUserMedia({
                        video: true,
                        audio: true
                    }, function (stream) {
                        localStream = stream;

                        localVideoElm.srcObject = stream;
                        $(localVideoElm).width(800);

                    }, function (error) {
                        console.log("访问用户媒体设备失败：", error.name, error.message);
                    })
                } else { alert('您的浏览器不兼容'); }
            }

        }

        function PRInit(parterName, createOffer) {

            pc[parterName] = new RTCPeerConnection(iceServer);

            //本地流
            if (localStream) {

                localStream.getTracks().forEach((track) => {
                    pc[parterName].addTrack(track, localStream);//should trigger negotiationneeded event
                });

            } else {

                if (canGetUserMediaUse()) {
                    getUserMedia({
                        video: true,
                        audio: false
                    }, function (stream) {
                        localStream = stream;
                        localVideoElm.srcObject = stream;
                        $(localVideoElm).width(800);

                        stream.getTracks().forEach((track) => {
                            pc[parterName].addTrack(track, stream);//should trigger negotiationneeded event
                        });
                    }, function (error) {
                        console.log("访问用户媒体设备失败：", error.name, error.message);
                    })
                } else { alert('您的浏览器不兼容'); }

            }
            //本地流创建完毕，发起SDP消息
            if (createOffer) {
                //每当WebRTC基础结构需要你重新启动会话协商过程时，都会调用此函数。它的工作是创建和发送一个请求，给被叫方，要求它与我们联系。
                pc[parterName].onnegotiationneeded = () => {
                    //https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/createOffer

                    pc[parterName].createOffer().then((offer) => {
                        return pc[parterName].setLocalDescription(offer);
                    }).then(() => {
                        //把发起者的描述信息通过Signal Server发送到接收者
                        socket.emit('sdp', {
                            type: 'video-offer',
                            description: pc[parterName].localDescription,
                            to: parterName,
                            sender: socketID
                        });
                    })
                };
            }

            //当向连接中添加磁道时，track 事件的此处理程序由本地WebRTC层调用。例如，可以将传入媒体连接到元素以显示它。详见 Receiving new streams 。
            pc[parterName].ontrack = (ev) => {
                let str = ev.streams[0];

                if (document.getElementById(`${parterName}-video`)) {
                    document.getElementById(`${parterName}-video`).srcObject = str;
                } else {
                    let newVideo = document.createElement('video');
                    newVideo.id = `${parterName}-video`;
                    newVideo.autoplay = true;
                    newVideo.controls = true;
                    newVideo.className = 'remote-video';
                    newVideo.srcObject = str;

                    document.getElementById('videos').appendChild(newVideo);
                }
            }

            //当需要你通过信令服务器将一个ICE候选发送给另一个对等端时，本地ICE层将会调用你的 icecandidate 事件处理程序。有关更多信息，请参阅Sending ICE candidates 以查看此示例的代码。
            pc[parterName].onicecandidate = ({ candidate }) => {
                socket.emit('ice candidates', { candidate: candidate, to: parterName, sender: socketID });
            }

        }

        function closeVideoCall(parterName) {
            if (localVideoElm.srcObject) {
                //localVideoElm.srcObject.getTracks().forEach(track => track.stop());
            }
            if (document.getElementById(`${parterName}-video`)) {
                document.getElementById(`${parterName}-video`).srcObject.getTracks().forEach(track => track.stop());
            }

            pc[parterName].ontrack = null;
            pc[parterName].onremovetrack = null;
            pc[parterName].onremovestream = null;
            pc[parterName].onicecandidate = null;
            pc[parterName].oniceconnectionstatechange = null;
            pc[parterName].onsignalingstatechange = null;
            pc[parterName].onicegatheringstatechange = null;
            pc[parterName].onnegotiationneeded = null;
            pc[parterName].close();

            pc[parterName] = null;
        }



        $('document').ready(() => {

            $('#capture').click(() => {
                let video = localVideoElm//原生dom
                let isPlaying = !(video.paused || video.ended || video.seeking || video.readyState < video.HAVE_FUTURE_DATA)

                if (isPlaying) {
                    let canvas = $('#capture-canvas')
                    canvas.attr('width', localVideoElm.clientWidth);//设置canvas的宽度
                    canvas.attr('height', localVideoElm.clientHeight);//设置canvas的高度

                    let img = $('<img>')
                    img.attr('width', localVideoElm.clientWidth);//设置图像的宽度
                    img.attr('height', localVideoElm.clientHeight);//设置图像的高度

                    //canvas[0] //jQuery对象转dom
                    var context = canvas[0].getContext('2d');
                    //在canvas上绘图，其绘图坐标为0,0; 
                    //绘图大小为摄像头内容的宽度，高度（全局绘制，你可以改变这些值试试效果）。
                    context.drawImage(localVideoElm, 0, 0, localVideoElm.clientWidth, localVideoElm.clientHeight);
                    //根据canvas内容进行编码，并赋值到图片上
                    var data = canvas[0].toDataURL('image/png');
                    img.attr('src', data);
                    //插入到id为capture-list的有序列表里
                    $('#capture-list').append($('<li></li>').html(img));
                }
            })

        });


        var socket = io();


        //处理请求 ：首先，它需要创建自己的RTCPeerConnection 并添加包含麦克风和网络摄像头的音频和视频的磁道。其次，它需要对收到的请求进行处理，构建并返回应答。
        socket.on('sdp', (data) => {
            console.log('sdp');
            if (data.description.type === 'offer') {
                //PRInit(socketID, false);
                PRInit(data.sender, false);
                //把发送者(offer)的描述，存储在接收者的remoteDesc中。
                let desc = new RTCSessionDescription(data.description);
                pc[data.sender].setRemoteDescription(desc).then(() => {

                    pc[data.sender].createAnswer().then((answer) => {
                        return pc[data.sender].setLocalDescription(answer);
                    }).then(() => {
                        socket.emit('sdp', {
                            type: 'video-answer',
                            description: pc[data.sender].localDescription,
                            to: data.sender,
                            sender: socketID
                        });

                    }).catch();//catch error function empty

                })
            } else if (data.description.type === 'answer') {
                //应答
                pc[data.sender].setRemoteDescription(new RTCSessionDescription(data.description));
            }

        });

        //接收ICE候选
        socket.on('ice candidates', (data) => {
            console.log('ice candidate: ' + data.candidate);
            //{ candidate: candidate, to: partnerName, sender: socketID }
            if (data.candidate) {
                var candidate = new RTCIceCandidate(data.candidate);

                pc[data.sender].addIceCandidate(candidate).catch();//catch err function empty
            }

        });

        var currentConnect = 0;

        socket.on('connect', () => {
            socketID = socket.id;//socket.io.engine.id;
            console.log('hello local user: ' + socket.id);

            pc.push(socketID);
            init(socketID);

            $('#me').text('hello ' + socket.id);

            //加入时候向服务器发送订阅/注册?
            socket.emit('new user greet', { msg: 'hello' });


            socket.on('need connect', (data) => {
                console.log('someone need connect' + data.msg);
                console.log(data.sender);
                //                let li = $('<li></li>').text(data.sender).attr('user-id', data.sender);

                let li = $('<li></li>').attr('user-id', data.sender);
                let dl = $('<dl></dl>').appendTo(li)
                let name = $('<dt></dt>').text(data.sender).appendTo(dl)
                let call = $('<dd><button class="call">发起通话</button></dd>').appendTo(dl);
                let hangup = $('<dd><button class="hangup">挂断</button></dd>').appendTo(dl);

                $('#user-list').append(li);

                call.click(function () {
                    console.log($(this).parent().parent().attr('user-id'))
                    //PRInit(socketID, false);
                    PRInit($(this).parent().parent().attr('user-id'), true);
                })

                hangup.click(function () {
                    closeVideoCall($(this).parent().parent().attr('user-id'));
                })


                socket.emit('ok we connect', { sender: socketID, receiver: data.sender });

                pc.push(data.sender);

                currentConnect++;

            })

            socket.on('user disconnected', (user) => {
                currentConnect
                console.log('user disconnected ' + user)
                //console.log('disconnect' +  $('#user-list li[user-id="' + user + '"]').text())
                $('#user-list li[user-id="' + user + '"]').remove();

                if (currentConnect > 0)
                    currentConnect--;
            })

            socket.on('ok we connect', (data) => {
                console.log('ok we connect ' + data.sender);
                let li = $('<li></li>').text(data.sender).attr('user-id', data.sender);
                $('#user-list').append(li);

                pc.push(data.sender);

                console.log(pc);

                currentConnect++;
            })

        });


    </script>


</body>

</html>